<?php
use frontend\modules\yii2\widgets\layouts\markdown\Markdown;
use frontend\modules\yii2\widgets\layouts\example\Example;
use yii\bootstrap\Button;
use yii\bootstrap\Alert;

$this->title = 'RESTful API 内容协商应用举例';
$this->registerMetaTag([
    'name' => 'keywords',
    'content' => 'yii,git,docker,Yii2,install,容器,镜像,框架,安装,教程,codelab,实操,RESTful api,内容协商,Content Negotiator'
]);
$this->registerMetaTag([
    'name' => 'description',
    'content' => '在本实验中将用一个完整的案例演示如何使用 RESTful API 的内容协商功能，希望读者通过此案例了解内容协商的概念。'
]);
$this->registerMetaTag([
    'name' => 'author',
    'content' => 'chenzhiwei'
]);

$breadcrumb = 'RESTful API 内容协商应用举例';

$steps = [
    [
        "label" => "准备工作",
        "content" => <<<CODE
## 1. 准备工作
您需要一台安装有Docker的电脑或者服务器，我们建议您在Linux系统下进行本次实验。

### 1.1 安装Docker
Linux系统下请在命令行中执行如下的命令即可完成Docker的安装：
```shell
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
```

### 1.2 安装Yii
我们采用之前制作的自定义镜像安装Yii，如果您不了解这个镜像的由来，前往[《基于PHP官方发布的容器安装Yii2.0框架》](/codelab/install/docker/custom)一节。

**下载Yii框架**  

为了节约时间，我们跳过依赖下载和Yii框架初始化的部分，下载可以直接运行的Yii框架：
```php
// 设置目录
mkdir -p /srv/docker/www
cd /srv/docker/www

// 下载Yii框架
git clone -b 2.0.43.1.1 https://gitee.com/lv_frame/yii.git yii
// 如果你不想写代码，可以拉取本实验完整代码：
git clone -b content_negotiator https://gitee.com/lv_frame/yii.git yii

// 配置权限
chown www-data:www-data -R yii
chown o+w -R yii
```
> **注意：** `2.0.43.1.1`是在官方发布的`2.0.43`基础上完成依赖拉取和Yii初始化后制作的tag。

**运行容器**

我们采用之前在[《基于PHP官方发布的容器安装Yii2.0框架》](/codelab/install/docker/custom)一节中设计好的镜像来运行此Yii框架：
```php
cd /srv/docker/www/yii
docker run -itd --name php8 -v \$PWD:/var/www/html/yii -p 8080:80 lvzmen/php-fpm:8.0.7.1
```

### 1.3 查看安装结果
我们在运行容器的时候已将宿主机的`8080`端口映射到容器的`80`端口，因此直接访问宿主机的`8080`端口接口访问容器内的Yii，如果您能看到下面的页面，说明您已经成功安装好Yii了：
![Yii安装成功页面](/images/20211218154010.png "Yii安装成功页面")
  
CODE,
    ],
    [
        "label" => "Gii创建Api模块",
        "content" => <<<CODE
## 2. Gii创建Api模块
在一个网站应用中，Api常常只占全部功能的一部分，因此最好的方式就是创建一个模块（Module），将所有的Api都放到这个模块中管理。我们之前介绍过如果[用Gii创建控制器](/docs/structure/controller#gii-creation)，现在我们介绍如何用Gii创建模块。

### 2.1 开启Gii
要开启Gii需要配置两个地方：
- 开启IP访问权限：`frontend/config/main-local.php`
```php
if (YII_ENV_DEV) {
    \$config['bootstrap'][] = 'gii';
    \$config['modules']['gii'] = [
        'class' => 'yii\gii\Module',
        'allowedIPs' => ['*'] // 允许开发环境所有ip访问
    ];
}
```
- 开启URL美化：`frontend/config/main.php`
```php
return [
    ... ...
    'components' => [
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules' => [
            ],
        ],
        ... ...
    ],
];
```

访问`http://127.0.0.1:8080/gii`，如果出现如下页面表示Gii开启成功：
![Gii页面](/images/20211218213550.png "Gii页面")

### 2.2 创建Api模块
![创建Api模块页面](/images/20211218213936.png "创建Api模块页面")

### 2.3 配置模块
创建好模块后还不能立即访问，需要加入如下配置项：`frontend/config/main.php`
```php
return [
    ... ...
    'modules' => [
        'api' => [
            'class' => 'frontend\modules\api\Module',
        ],
    ],
];
```

### 2.4 创建成功
如果访问`http://127.0.0.1:8080/api`能出现如下页面，则说明Api模块创建成功：
![Api模块默认页面](/images/20211218214721.png "Api模块默认页面")

CODE

    ],
    [
        "label" => "配置内容协商",
        "content" => <<<CODE
## 3 配置内容协商
由于我们创建了模块来管理api，因此这个模块里的所有控制器都应该接受内容协商，所以内容协商的配置应该在模块公共文件`frontend/modules/api/Module.php`中设置：
```php
use yii\web\Response;
public function behaviors()
{
    return [
        [
            'class' => 'yii\\filters\ContentNegotiator',
            // if in a module, use the following IDs for user actions
            'only' => ['user/view', 'user/index'],
            'formats' => [
                'application/json' => Response::FORMAT_JSON,
                'application/xml' => Response::FORMAT_XML,
            ],
            'languages' => [
                'en',
                'de',
            ],
        ],
    ];
}
```

CODE

    ],
    [
        "label" => "创建接口",
        "content" => <<<CODE
## 4 创建接口
我们创建一个`User`控制器，获取`Info`数据：`frontend/modules/api/controllers/UserController.php`
```php
<?php
namespace frontend\modules\api\controllers;

use yii\\rest\Controller;

class UserController extends Controller
{

    public function behaviors()
    {
        // 删除父类中的内容协商配置，否则会覆盖Module中的配置，使Module中的配置失效
        \$behaviors = parent::behaviors();
        unset(\$behaviors["contentNegotiator"]);
        return \$behaviors;
    }

    public function actionInfo(\$id = 1)
    {
        return [
            "id" => 1,
            "username" => "lvzmen",
            "gender" => "male",
            "ago" => 30,
            "occupation" => "coder"
        ];
    }
}


```

CODE
    ],
    [
        "label" => "通过内容协商访问",
        "content" => <<<CODE
## 5 通过内容协商访问接口
我们之前设置好了内容协商的配置，所以可以让客户端来决定返回的数据格式（json或者xml），一般有两种方式来设置响应格式：
- `Accept`请求头设置法；
- `Get`参数`_format`设置法；

### 5.1 用请求头指定返回格式

**json格式返回：**
```PHP
curl -i -H "Accept:application/json" "http://127.0.0.1:8080/api/user/info"
HTTP/1.1 200 OK
Server: nginx/1.20.1
Date: Sun, 19 Dec 2021 02:46:55 GMT
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/8.0.7
Vary: Accept-Language

{"id":1,"username":"lvzmen","gender":"male","ago":30,"occupation":"coder"}
```
**xml格式返回：**
```php
curl -i -H "Accept:application/xml" "http://127.0.0.1:8080/api/user/info"
HTTP/1.1 200 OK
Server: nginx/1.20.1
Date: Sun, 19 Dec 2021 02:48:34 GMT
Content-Type: application/xml; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Powered-By: PHP/8.0.7
Vary: Accept
Vary: Accept-Language

<?xml version="1.0" encoding="UTF-8"?>
<response>
    <id>1</id>
    <username>lvzmen</username>
    <gender>male</gender>
    <ago>30</ago>
    <occupation>coder</occupation>
</response>
```

### 5.2 用GET指定返回格式
**json格式返回：**
```php
curl -i "http://127.0.0.1:8080/api/user/info?_format=json"
HTTP/1.1 200 OK
Server: nginx/1.20.1
Date: Sun, 19 Dec 2021 03:27:56 GMT
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/8.0.7
Vary: Accept
Vary: Accept-Language

{"id":1,"username":"lvzmen","gender":"male","ago":30,"occupation":"coder"}
```
**xml格式返回：**
```php
curl -i "http://127.0.0.1:8080/api/user/info?_format=xml"
HTTP/1.1 200 OK
Server: nginx/1.20.1
Date: Sun, 19 Dec 2021 03:28:01 GMT
Content-Type: application/xml; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Powered-By: PHP/8.0.7
Vary: Accept
Vary: Accept-Language

<?xml version="1.0" encoding="UTF-8"?>
<response>
    <id>1</id>
    <username>lvzmen</username>
    <gender>male</gender>
    <ago>30</ago>
    <occupation>coder</occupation>
</response>
```


CODE
    ],
    [
        "label" => "注意事项",
        "content" => <<<CODE
## 6 注意事项

### 6.1 注意配置覆盖
`yii\\rest\Controller`这个控制器中默认有配置内容协商的配置，因此如果你想在`Module`或者应用主体配置`bootstrap`中配置内容协商，就需要移除`yii\\rest\Controller`中的配置：
```php
namespace frontend\modules\api\controllers;

use yii\\rest\Controller;

class UserController extends Controller
{
    public function behaviors()
    {
        // 删除父类中的内容协商配置，否则会覆盖Module中的配置，使Module中的配置失效
        \$behaviors = parent::behaviors();
        unset(\$behaviors["contentNegotiator"]);
        return \$behaviors;
    }
}
```

### 6.2 formats的配置说明
- 如果在内容协商中没有配置`formats`，或者配置为空，则表示不对内容协商进行限制
```php
public function behaviors()
{
    return [
        [
            'class' => 'yii\filters\ContentNegotiator',
            // if in a module, use the following IDs for user actions
            'only' => ['user/info'],
            'formats' => [
                //'application/json' => Response::FORMAT_JSON,
                //'application/xml' => Response::FORMAT_XML,            
            ],
            'languages' => [
                'en',
                'de',
            ],
        ],
    ];
}
```
此时客户端指定`json`和`xml`返回格式都可正确返回。

- 禁止不在`formats`中存在的格式请求
```php
public function behaviors()
{
    return [
        [
            'class' => 'yii\filters\ContentNegotiator',
            // if in a module, use the following IDs for user actions
            'only' => ['user/info'],
            'formats' => [
                'application/json' => Response::FORMAT_JSON,
                //'application/xml' => Response::FORMAT_XML,            
            ],
            'languages' => [
                'en',
                'de',
            ],
        ],
    ];
}
```
上述配置不再允许`xml`的格式请求，否则会抛出异常：
```php
curl -i -H "Accept:application/xml" "http://127.0.0.1:8080/api/user/info"
HTTP/1.1 406 Not Acceptable
Server: nginx/1.20.1
Date: Sun, 19 Dec 2021 03:45:42 GMT
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/8.0.7
{"name":"Not Acceptable","message":"None of your requested content types is supported.","code":0,"status":406,"type":"yii\\web\\NotAcceptableHttpException"}
```

- 请求xml，返回json  
如果想设置无论请求json格式还是xml格式，都返回json格式的数据，则可以如下配置：
```php
public function behaviors()
{
    return [
        [
            'class' => 'yii\filters\ContentNegotiator',
            // if in a module, use the following IDs for user actions
            'only' => ['user/info'],
            'formats' => [
                'application/json' => Response::FORMAT_JSON, // json的请求依旧以json格式返回
                'application/xml' => Response::FORMAT_JSON,  // xml的请求以json格式返回
            ],
            'languages' => [
                'en',
                'de',
            ],
        ],
    ];
}
```
请求实例：
```php
curl -i -H "Accept:application/xml" "http://127.0.0.1:8080/api/user/info"
HTTP/1.1 200 OK
Server: nginx/1.20.1
Date: Sun, 19 Dec 2021 03:55:15 GMT
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/8.0.7
Vary: Accept
Vary: Accept-Language
{"id":1,"username":"lvzmen","gender":"male","ago":30,"occupation":"coder"}
```

CODE

    ],
    [
        "label" => "本实验完整代码",
        "content" => <<<CODE
## 7 本实验完整代码
为了便于大家在线翻阅代码，我们把本实验的代码打了tag，并推送到了Gitee：
```php
git clone -b content_negotiator https://gitee.com/lv_frame/yii.git yii
```
在线访问`https://gitee.com/lv_frame/yii/tags`：
![内容协商在线代码](/images/20211219121034.png "内容协商在线代码")

CODE

    ]
];

require "template.php";